homework 5, version 0

4.9 ms

Submission by: PoorRichRE (jazz@gmail.com)

13.1 ms
6.1 ms

Homework 5: Epidemic modeling II

18.S191, fall 2020

This notebook contains built-in, live answer checks! In some exercises you will see a coloured box, which runs a test case on your code, and provides feedback based on the result. Simply edit the code, run it, and the check runs again.

For MIT students: there will also be some additional (secret) test cases that will be run as part of the grading process, and we will look at your notebook and write comments.

Feel free to ask questions!

1.9 ms
student
16.1 ms

Let's create a package environment:

23.6 μs
263 ms
359 s

In this problem set, we will look at a simple spatial agent-based epidemic model: agents can interact only with other agents that are nearby. (In the previous homework any agent could interact with any other, which is not realistic.)

A simple approach is to use discrete space: each agent lives in one cell of a square grid. For simplicity we will allow no more than one agent in each cell, but this requires some care to design the rules of the model to respect this.

We will adapt some functionality from the previous homework. You should copy and paste your code from that homework into this notebook.

58.4 μs




1.3 μs

Exercise 1: Wandering at random in 2D

In this exercise we will implement a random walk on a 2D lattice (grid). At each time step, a walker jumps to a neighbouring position at random (i.e. chosen with uniform probability from the available adjacent positions).

19.8 μs

Exercise 1.1

We define a struct type Coordinate that contains integers x and y.

21.9 μs
2.3 ms

👉 Construct a Coordinate located at the origin.

8.2 μs
origin
39.7 ms

Got it!

Good job!

6.5 ms

👉 Write a function make_tuple that takes an object of type Coordinate and returns the corresponding tuple (x, y). Boring, but useful later!

34.6 μs
make_tuple (generic function with 1 method)
30.4 μs

Got it!

Great!

13.7 ms

Exercise 1.2

In Julia, operations like + and * are just functions, and they are treated like any other function in the language. The only special property you can use the infix notation: you can write

1 + 2

instead of

+(1, 2)

(There are lots of special 'infixable' function names that you can use for your own functions!)

When you call it with the prefix notation, it becomes clear that it really is 'just another function', with lots of predefined methods.

36.3 μs
3
13.1 μs
+ (generic function with 206 methods)
2.5 μs

Extending + in the wild

Because it is a function, we can add our own methods to it! This feature is super useful in general languages like Julia and Python, because it lets you use familiar syntax (a + b*c) on objects that are not necessarily numbers!

One example we've see before is the RGB type in Homework 1. You are able to do:

0.5 * RGB(0.1, 0.7, 0.6)

to multiply each color channel by 0.5. This is possible because Images.jl wrote a method:

*(::Real, ::AbstractRGB)::AbstractRGB

👉 Implement addition on two Coordinate structs by adding a method to Base.:+

21.6 μs
332 μs
24.7 ms

Pluto has some trouble here, you need to manually re-run the cell above!

14.4 μs

Got it!

Good job!

64.0 ms

Exercise 1.3

In our model, agents will be able to walk in 4 directions: up, down, left and right. We can define these directions as Coordinates.

21.4 μs
possible_moves
64.3 ms

👉 rand(possible_moves) gives a random possible move. Add this to the coordinate Coordinate(4,5) and see that it moves to a valid neighbor.

9.1 μs
62.9 ms

We are able to make a Coordinate perform one random step, by adding a move to it. Great!

👉 Write a function trajectory that calculates a trajectory of a Wanderer w when performing n steps., i.e. the sequence of positions that the walker finds itself in.

Possible steps:

  • Use rand(possible_moves, n) to generate a vector of n random moves. Each possible move will be equally likely.

  • To compute the trajectory you can use either of the following two approaches:

    1. 🆒 Use the function accumulate (see the live docs for accumulate). Use + as the function passed to accumulate and the w as the starting value (init keyword argument).

    2. Use a for loop calling +.

7.3 ms
Main.workspace3.trajectory
25.2 ms
test_trajectory
985 ms
11.4 s

Got it!

Awesome!

278 ms
plot_trajectory! (generic function with 1 method)
197 μs
535 ms

Exercise 1.4

👉 Plot 10 trajectories of length 1000 on a single figure, all starting at the origin. Use the function plot_trajectory! as demonstrated above.

Remember from last week that you can compose plots like this:

let
    # Create a new plot with aspect ratio 1:1
    p = plot(ratio=1)

    plot_trajectory!(p, test_trajectory)      # plot one trajectory
    plot_trajectory!(p, another_trajectory)   # plot the second one
    ...

    p
end
18.3 μs
172 ms
90.5 μs

Exercise 1.5

Agents live in a box of side length 2L, centered at the origin. We need to decide (i.e. model) what happens when they reach the walls of the box (boundaries), in other words what kind of boundary conditions to use.

One relatively simple boundary condition is a collision boundary:

Each wall of the box is a wall, modelled using "collision": if the walker tries to jump beyond the wall, it ends up at the position inside the box that is closest to the goal.

👉 Write a function collide_boundary which takes a Coordinate c and a size L, and returns a new coordinate that lies inside the box (i.e. [L,L]×[L,L]), but is closest to c. This is similar to extend_mat from Homework 1.

27.9 μs
collide_boundary (generic function with 1 method)
90.8 μs
46.9 ms

Exercise 1.6

👉 Implement a 3-argument method of trajectory where the third argument is a size. The trajectory returned should be within the boundary (use reflect_boundary from above). You can still use accumulate with an anonymous function that makes a move and then reflects the resulting coordinate, or use a for loop.

38.5 μs
Main.workspace3.trajectory
148 μs




1.4 μs

Exercise 2: Wanderering Agents

In this exercise we will create Agents which have a location as well as some infection state information.

Let's define a type Agent. Agent contains a position (of type Coordinate), as well as a status of type InfectionStatus (as in Homework 4).)

(For simplicity we will not use a num_infected field, but feel free to do so!)

13.1 μs
36.6 ms
7.4 μs
Agent
2.6 ms

Exercise 2.1

👉 Write a function initialize that takes parameters N and L, where N is the number of agents abd 2L is the side length of the square box where the agents live.

It returns a Vector of N randomly generated Agents. Their coordinates are randomly sampled in the [L,L]×[L,L] box, and the agents are all susceptible, except one, chosen at random, which is infectious.

33.7 μs
initialize (generic function with 1 method)
100 μs
99.7 ms

Got it!

Great!

349 ms
color (generic function with 1 method)
66.9 μs
position (generic function with 2 methods)
38.0 μs
color (generic function with 3 methods)
445 μs

Exercise 2.2

👉 Write a function visualize that takes in a collection of agents as argument, and the box size L. It should plot a point for each agent at its location, coloured according to its status.

You can use the keyword argument c=color.(agents) inside your call to the plotting function make the point colors correspond to the infection statuses. Don't forget to use ratio=1.

9.2 μs
visualize (generic function with 1 method)
147 μs
789 ms

Exercise 3: Spatial epidemic model – Dynamics

Last week we wrote a function interact! that takes two agents, agent and source, and an infection of type InfectionRecovery, which models the interaction between two agent, and possibly modifies agent with a new status.

This week, we define a new infection type, CollisionInfectionRecovery, and a new method that is the same as last week, except it only infects agent if agents.position==source.position.

10.0 μs
12.9 μs
2.2 ms

Write a function interact! that takes two Agents and a CollisionInfectionRecovery, and:

  • If the agents are at the same spot, causes a susceptible agent to communicate the desease from an infectious one with the correct probability.

  • if the first agent is infectious, it recovers with some probability

20.8 μs
interact! (generic function with 1 method)
78.7 μs
244 μs

Exercise 3.1

Your turn!

👉 Write a function step! that takes a vector of Agents, a box size L and an infection. This that does one step of the dynamics on a vector of agents.

  • Choose an Agent source at random.

  • Move the source one step, and use collide_boundary to ensure that our agent stays within the box.

  • For all other agents, call interact!(other_agent, source, infection).

  • return the array agents again.

19.4 μs
1.1 ms
rand_move! (generic function with 1 method)
60.6 μs
45.4 ms
step! (generic function with 1 method)
80.3 μs
6.1 ms

Exercise 3.2

If we call step! N times, then every agent will have made one step, on average. Let's call this one sweep of the simulation.

👉 Create a before-and-after plot of ksweeps=1000 sweeps.

  • Initialize a new vector of agents (N=50, L=40, infection is given as pandemic below).

  • Plot the state using visualize, and save the plot as a variable plot_before.

  • Run k_sweeps sweeps.

  • Plot the state again, and store as plot_after.

  • Combine the two plots into a single figure using

plot(plot_before, plot_after)
20.3 μs
pandemic
6.3 ms
85.1 ms
sweep! (generic function with 1 method)
47.7 μs
1.3 s

Exercise 3.3

Every time that you move the slider, a completely new simulation is created an run. This makes it hard to view the progress of a single simulation over time. So in this exercise, we we look at a single simulation, and plot the S, I and R curves.

👉 Plot the SIR curves of a single simulation, with the same parameters as in the previous exercise. Use k_sweep_max = 10000 as the total number of sweeps.

9.1 μs
k_sweep_max
10000
2.8 μs
simulation (generic function with 1 method)
144 μs
11.0 s
repeat_simulations (generic function with 1 method)
129 μs
plot_repeated_sims (generic function with 1 method)
159 μs
plot_repeated_sims2 (generic function with 1 method)
168 μs

Hint

After every sweep, count the values S, I and R and push! them to 3 arrays.

150 ms

Exercise 3.4 (optional)

Let's make our plot come alive! There are two options to make our visualization dynamic:

👉1️⃣ Precompute one simulation run and save its intermediate states using deepcopy. You can then write an interactive visualization that shows both the state at time t (using visualize) and the history of S, I and R from time 0 up to time t. t is controlled by a slider.

👉2️⃣ Use @gif from Plots.jl to turn a sequence of plots into an animation. Be careful to skip about 50 sweeps between each animation frame, otherwise the GIF becomes too large.

This an optional exercise, and our solution to 2️⃣ is given below.

9.5 μs
47.1 s
77.9 ms

Hint

let
    N = 50
    L = 40

    x = initialize(N, L)
    
    # initialize to empty arrays
    Ss, Is, Rs = Int[], Int[], Int[]
    
    Tmax = 200
    
    @gif for t in 1:Tmax
        for i in 1:50N
            step!(x, L, pandemic)
        end

        #... track S, I, R in Ss Is and Rs
        
        left = visualize(x, L)
    
        right = plot(xlim=(1,Tmax), ylim=(1,N), size=(600,300))
        plot!(right, 1:t, Ss, color=color(S), label="S")
        plot!(right, 1:t, Is, color=color(I), label="I")
        plot!(right, 1:t, Rs, color=color(R), label="R")
    
        plot(left, right)
    end
end
10.0 μs

Exercise 3.5

👉 Using L=20 and N=100, experiment with the infection and recovery probabilities until you find an epidemic outbreak. (Take the recovery probability quite small.) Modify the two infections below to match your observations.

8.1 μs
p_recovery0
5.0e-5
3.0 μs
0.15
345 ms
30.4 s
0.55
102 μs
23.5 s
causes_outbreak
9.3 μs
does_not_cause_outbreak
9.5 μs

Exercise 3.6

👉 With the parameters of Exercise 3.2, run 50 simulations. Plot S, I and R as a function of time for each of them (with transparency!). This should look qualitatively similar to what you saw in the previous homework. You probably need different p_infection and p_recovery values from last week. Why?

8.3 μs
68.9 s
need_different_parameters_because

The nature in which agents are interacting is fundamentally different, meaning the transmission of the diseases is fundamentally different. In the previous homework, agents interacted based on drawing from a uniform propbability distribution. In this homework, agents are interacting based on a random walk within a bounded space

16.5 μs




995 ns

Exercise 4: Effect of socialization

In this exercise we'll modify the simple mixing model. Instead of a constant mixing probability, i.e. a constant probability that any pair of people interact on a given day, we will have a variable probability associated with each agent, modelling the fact that some people are more or less social or contagious than others.

13.1 μs

Exercise 4.1

We create a new agent type SocialAgent with fields position, status, num_infected, and social_score. The attribute social_score represents an agent's probability of interacting with any other agent in the population.

11.3 μs
SocialAgent
12.6 ms

define the position and color methods for SocialAgent as we did for Agent. This will allow the visualize function to work. on both kinds of Agents

18.6 μs

👉 Create a function initialize_social that takes N and L, and creates N agents within a 2L x 2L box, with social_scores chosen from 10 equally-spaced between 0.1 and 0.5. (see LinRange)

10.4 μs
initialize_social (generic function with 1 method)
84.3 μs

Now that we have 2 agent types

  1. let's create an AbstractAgent type

  2. Go back in the notebook and make the agent types a subtype of AbstractAgent.

24.2 μs

Exercise 4.2

Not all two agents who end up in the same grid point may actually interact in an infectious way – they may just be passing by and do not create enough exposure for communicating the disease.

👉 Write a new interact! method on SocialAgent which adds together the social_scores for two agents and uses that as the probability that they interact in a risky way. Only if they interact in a risky way, the infection is transmitted with the usual probability.

25.7 μs
interact! (generic function with 2 methods)
98.2 μs
Main.workspace52.SocialAgent1SocialAgent status
I::InfectionStatus = 1
num_infected
1
positionsocial_score
0.45555555555555555
path
2SocialAgent status
I::InfectionStatus = 1
num_infected
0
positionsocial_score
0.32222222222222224
path
799 ms
SocialAgent status
S::InfectionStatus = 0
num_infected
0
positionsocial_score
0.4111111111111111
pathMain.workspace3.Coordinate1234567891011
175 μs
7.6 μs

Make sure step!, position, color, work on the type SocialAgent. If step! takes an untyped first argument, it should work for both Agent and SocialAgent types without any changes. We actually only need to specialize interact! on SocialAgent.

Exercise 4.3

👉 Plot the SIR curves of the resulting simulation.

N = 50; L = 40; number of steps = 200

In each step call step! 50N times.

23.5 μs
68.6 s

Exercise 4.4

👉 Make a scatter plot showing each agent's social_score on one axis, and the num_infected from the simulation in the other axis. Run this simulation several times and comment on the results.

8.1 μs
plot_social_scores (generic function with 1 method)
114 μs
126 ms
13.6 ms

👉 Run a simulation for 100 steps, and then apply a "lockdown" where every agent's social score gets multiplied by 0.25, and then run a second simulation which runs on that same population from there. What do you notice? How does changing this factor form 0.25 to other numbers affect things?

7.5 μs
47.5 s

Incorporating a lockdown factor of 0.25 completely eliminates transmission. Loosening the lockdown factor to 0.75 results in a small uptick in transmission but it avoids a pandemic outcome.

13.6 μs

Exercise 5: (Optional) Effect of distancing

We can use a variant of the above model to investigate the effect of the mis-named "social distancing" (we want people to be socially close, but physically distant).

In this variant, we separate out the two effects "infection" and "movement": an infected agent chooses a neighbouring site, and if it finds a susceptible there then it infects it with probability pI. For simplicity we can ignore recovery.

Separately, an agent chooses a neighbouring site to move to, and moves there with probability pM if the site is vacant. (Otherwise it stays where it is.)

When pM=0, the agents cannot move, and hence are completely quarantined in their original locations.

👉 How does the disease spread in this case?

40.8 μs
640 ns

👉 Run the dynamics repeatedly, and plot the sites which become infected.

6.6 μs
635 ns

👉 How does this change as you increase the density ρ=N/(L2) of agents? Start with a small density.

This is basically the site percolation model.

When we increase pM, we allow some local motion via random walks.

17.8 μs
755 ns

👉 Investigate how this leaky quarantine affects the infection dynamics with different densities.

9.4 μs
634 ns




1.4 μs
8.8 μs

Function library

Just some helper functions used in the notebook.

18.0 μs
hint (generic function with 1 method)
50.8 μs
almost (generic function with 1 method)
32.1 μs
still_missing (generic function with 2 methods)
97.0 μs
keep_working (generic function with 2 methods)
57.2 μs
yays
78.6 ms
correct (generic function with 2 methods)
50.7 μs
not_defined (generic function with 1 method)
50.9 μs

UndefVarError: starting not defined

  1. top-level scope@Local: 1
---
4.7 ms